# -*- coding: utf-8 -*-
"""
    xfree.auth.views
    ~~~~~~~~~~~~~~~~~~~~~
    文件描述：此视图提供用户认证、注册以及用户丢失密码时重置密码的功能。
    作者：xfree
    邮箱：cy_83k@qq.com
"""

import logging
from datetime import datetime

from flask import Blueprint, current_app, flash, g, redirect, request, url_for
from flask.views import MethodView
from flask_babelplus import gettext as _
from flask_login import (
    confirm_login,
    current_user,
    login_fresh,
    login_required,
    login_user,
    logout_user,
)

# 导入各种表单类
from xfree.auth.forms import (
    AccountActivationForm,
    ForgotPasswordForm,
    LoginForm,
    LoginRecaptchaForm,
    ReauthForm,
    RegisterForm,
    RequestActivationForm,
    ResetPasswordForm,
)
from xfree.extensions import db, limiter
from xfree.utils.helpers import (
    anonymous_required,
    enforce_recaptcha,
    format_timedelta,
    get_available_languages,
    redirect_or_next,
    register_view,
    registration_enabled,
    render_template,
    requires_unactivated,
)
from xfree.utils.settings import xfree_config

# 从相关模块导入异常和类
from ..core.auth.authentication import StopAuthentication
from ..core.auth.registration import UserRegistrationInfo
from ..core.exceptions import PersistenceError, StopValidation, ValidationError
from ..core.tokens import TokenError
from .plugins import impl
from .services import (
    account_activator_factory,
    authentication_manager_factory,
    reauthentication_manager_factory,
    registration_service_factory,
    reset_service_factory,
)

# 获取日志记录器
logger = logging.getLogger(__name__)


class Logout(MethodView):
    """
    登出视图类
    """
    # 应用限流豁免和登录要求装饰器
    decorators = [limiter.exempt, login_required]

    def get(self):
        """
        处理GET请求，执行用户登出操作

        :return: 重定向到论坛首页，并显示登出成功的提示
        """
        logout_user()
        flash(_("Logged out"), "success")
        return redirect(url_for("forum.index"))


class Login(MethodView):
    """
    登录视图类
    """
    # 应用匿名用户要求装饰器

    decorators = [anonymous_required]

    def __init__(self, authentication_manager_factory):
        """
        初始化方法

        :param authentication_manager_factory: 认证管理器工厂对象
        """
        self.authentication_manager_factory = authentication_manager_factory

    def form(self):
        """
        根据是否需要reCAPTCHA返回相应的登录表单

        :return: 登录表单对象
        """
        if enforce_recaptcha(limiter):
            return LoginRecaptchaForm()
        return LoginForm()

    def get(self):
        """
        处理GET请求，显示登录页面

        :return: 渲染后的登录页面模板
        """
        return render_template("auth/login.html", form=self.form())

    def post(self):
        """
        处理POST请求，验证登录表单并进行登录操作

        :return: 如果登录成功，重定向到论坛首页或下一个页面；如果登录失败，重新渲染登录页面并显示错误信息
        """

        form = self.form()
        if form.validate_on_submit():
            auth_manager = self.authentication_manager_factory()
            try:
                user = auth_manager.authenticate(
                    identifier=form.login.data, secret=form.password.data
                )
                login_user(user, remember=form.remember_me.data)
                return redirect_or_next(url_for("forum.index"), False)
            except StopAuthentication as e:
                flash(e.reason, "danger")
            except Exception:
                flash(_("处理登录时无法恢复的错误！"))

        return render_template("auth/login.html", form=form)


class Reauth(MethodView):
    """
    重新认证视图类
    """
    # 应用登录要求和限流豁免装饰器
    decorators = [login_required, limiter.exempt]
    form = ReauthForm

    def __init__(self, reauthentication_factory):
        """
        初始化方法

        :param reauthentication_factory: 重新认证工厂对象
        """
        self.reauthentication_factory = reauthentication_factory

    def get(self):
        """
        处理GET请求，如果用户登录状态不新鲜，显示重新认证页面

        :return: 如果登录状态不新鲜，渲染后的重新认证页面模板；否则重定向到用户页面
        """
        if not login_fresh():
            return render_template("auth/reauth.html", form=self.form())
        return redirect_or_next(current_user.url)

    def post(self):
        """
        处理POST请求，验证重新认证表单并进行重新认证操作

        :return: 如果重新认证成功，确认登录并重定向到用户页面；如果重新认证失败，重新渲染重新认证页面并显示错误信息
        """
        form = self.form()
        if form.validate_on_submit():
            reauth_manager = self.reauthentication_factory()
            try:
                reauth_manager.reauthenticate(
                    user=current_user, secret=form.password.data
                )
                confirm_login()
                flash(_("Reauthenticated."), "success")
                return redirect_or_next(current_user.url)
            except StopAuthentication as e:
                flash(e.reason, "danger")
            except Exception:
                flash(_("Unrecoverable error while handling reauthentication"))
                raise

        return render_template("auth/reauth.html", form=form)


class Register(MethodView):
    """
    注册视图类
    """
    # 应用匿名用户要求和注册启用检查装饰器
    decorators = [anonymous_required, registration_enabled]

    def __init__(self, registration_service_factory):
        """
        初始化方法

        :param registration_service_factory: 注册服务工厂对象
        """
        self.registration_service_factory = registration_service_factory

    def form(self):
        """
        获取注册表单，并设置语言选择字段的选项和默认值

        :return: 注册表单对象
        """
        current_app.pluggy.hook.xfree_form_registration(form=RegisterForm)
        form = RegisterForm()

        form.language.choices = get_available_languages()
        form.language.default = xfree_config["DEFAULT_LANGUAGE"]
        form.process(request.form)  # 因为覆盖了默认值，所以需要处理表单数据
        return form

    def get(self):
        """
        处理GET请求，显示注册页面

        :return: 渲染后的注册页面模板
        """
        return render_template("auth/register.html", form=self.form())

    def post(self):
        """
        处理POST请求，验证注册表单并进行注册操作

        :return: 如果注册成功，重定向到论坛首页或下一个页面；如果注册失败，重新渲染注册页面并显示错误信息
        """
        form = self.form()
        if form.validate_on_submit():
            registration_info = UserRegistrationInfo(
                username=form.username.data,
                password=form.password.data,
                group=4,
                email=form.email.data,
                language=form.language.data,
            )

            service = self.registration_service_factory()
            try:
                service.register(registration_info)
            except StopValidation as e:
                form.populate_errors(e.reasons)
                return render_template("auth/register.html", form=form)
            except PersistenceError:
                logger.exception("Database error while persisting user")
                flash(
                    _("Could not process registration due" "to an unrecoverable error"),
                    "danger",
                )

                return render_template("auth/register.html", form=form)

            current_app.pluggy.hook.xfree_event_user_registered(
                username=registration_info.username
            )
            return redirect_or_next(url_for("forum.index"))

        return render_template("auth/register.html", form=form)


class ForgotPassword(MethodView):
    """
    忘记密码视图类
    """
    # 应用匿名用户要求装饰器
    decorators = [anonymous_required]
    form = ForgotPasswordForm

    def __init__(self, password_reset_service_factory):
        """
        初始化方法

        :param password_reset_service_factory: 密码重置服务工厂对象
        """
        self.password_reset_service_factory = password_reset_service_factory

    def get(self):
        """
        处理GET请求，显示忘记密码页面

        :return: 渲染后的忘记密码页面模板
        """
        return render_template("auth/forgot_password.html", form=self.form())

    def post(self):
        """
        处理POST请求，验证忘记密码表单并进行密码重置初始化操作

        :return: 如果密码重置初始化成功，显示邮件已发送的提示并重新定向到忘记密码页面；如果验证失败，重新渲染忘记密码页面并显示错误信息
        """
        form = self.form()
        if form.validate_on_submit():
            try:
                service = self.password_reset_service_factory()
                service.initiate_password_reset(form.email.data)
            except ValidationError:
                flash(
                    _(
                        "You have entered an username or email address that "
                        "is not linked with your account."
                    ),
                    "danger",
                )
            else:
                flash(_("Email sent! Please check your inbox."), "info")
                return redirect(url_for("auth.forgot_password"))

        return render_template("auth/forgot_password.html", form=form)


class ResetPassword(MethodView):
    """
    重置密码视图类
    """
    # 应用匿名用户要求装饰器
    decorators = [anonymous_required]
    form = ResetPasswordForm

    def __init__(self, password_reset_service_factory):
        """
        初始化方法

        :param password_reset_service_factory: 密码重置服务工厂对象
        """
        self.password_reset_service_factory = password_reset_service_factory

    def get(self, token):
        """
        处理GET请求，显示重置密码页面，并设置令牌字段值

        :param token: 密码重置令牌
        :return: 渲染后的重置密码页面模板
        """
        form = self.form()
        form.token.data = token
        return render_template("auth/reset_password.html", form=form)

    def post(self, token):
        """
        处理POST请求，验证重置密码表单并进行密码重置操作

        :param token: 密码重置令牌
        :return: 如果密码重置成功，显示密码已更新的提示并重新定向到登录页面；如果验证失败或出现其他错误，重新渲染重置密码页面并显示相应错误信息
        """
        form = self.form()
        if form.validate_on_submit():
            try:
                service = self.password_reset_service_factory()
                service.reset_password(token, form.email.data, form.password.data)
            except TokenError as e:
                flash(e.reason, "danger")
                return redirect(url_for("auth.forgot_password"))
            except StopValidation as e:
                form.populate_errors(e.reasons)
                form.token.data = token
                return render_template("auth/reset_password.html", form=form)
            except Exception:
                logger.exception("Error when resetting password")
                flash(_("Error when resetting password"))
                return redirect(url_for("auth.forgot_password"))
            finally:
                try:
                    db.session.commit()
                except Exception:
                    logger.exception(
                        "Error while finalizing database when resetting password"
                    )
                    db.session.rollback()

            flash(_("Your password has been updated."), "success")
            return redirect(url_for("auth.login"))

        form.token.data = token
        return render_template("auth/reset_password.html", form=form)


class RequestActivationToken(MethodView):
    """
    请求激活令牌视图类
    """
    # 应用未激活用户要求装饰器
    decorators = [requires_unactivated]
    form = RequestActivationForm

    def __init__(self, account_activator_factory):
        """
        初始化方法

        :param account_activator_factory: 账户激活器工厂对象
        """
        self.account_activator_factory = account_activator_factory

    def get(self):
        """
        处理GET请求，显示请求账户激活页面

        :return: 渲染后的请求账户激活页面模板
        """
        return render_template("auth/request_account_activation.html", form=self.form())

    def post(self):
        """
        处理POST请求，验证请求激活表单并进行账户激活请求操作

        :return: 如果账户激活请求成功，显示新的账户激活令牌已发送的提示并重新定向到论坛首页；如果验证失败，重新渲染请求账户激活页面并显示错误信息
        """
        form = self.form()
        if form.validate_on_submit():
            activator = self.account_activator_factory()
            try:
                activator.initiate_account_activation(form.email.data)
            except ValidationError as e:
                form.populate_errors([(e.attribute, e.reason)])
            else:
                flash(
                    _(
                        "A new account activation token has been sent to "
                        "your email address."
                    ),
                    "success",
                )
                return redirect(url_for("forum.index"))

        return render_template("auth/request_account_activation.html", form=form)


class AutoActivateAccount(MethodView):
    """
    自动激活账户视图类
    """
    # 应用未激活用户要求装饰器
    decorators = [requires_unactivated]

    def __init__(self, account_activator_factory):
        """
        初始化方法

        :param account_activator_factory: 账户激活器工厂对象
        """
        self.account_activator_factory = account_activator_factory

    def get(self, token):
        """
        处理GET请求，尝试使用令牌自动激活账户

        :param token: 账户激活令牌
        :return: 如果激活成功，显示账户已激活的提示并重新定向到论坛首页；如果出现错误，显示相应错误提示并根据情况进行重定向或返回
        """
        activator = self.account_activator_factory()

        try:
            activator.activate_account(token)
        except TokenError as e:
            flash(e.reason, "danger")
        except ValidationError as e:
            flash(e.reason, "danger")
            return redirect(url_for("forum.index"))

        else:
            try:
                db.session.commit()
            except Exception:
                logger.exception("Database error while activating account")
                db.session.rollback()
                flash(
                    _(
                        "Could not activate account due to an unrecoverable error"
                    ),
                    "danger",
                )

                return redirect(url_for("auth.request_activation_token"))

            flash(
                _("Your account has been activated and you can now login."), "success"
            )
            return redirect(url_for("forum.index"))

        return redirect(url_for("auth.activate_account"))


class ActivateAccount(MethodView):
    """
    激活账户视图类
    """
    # 应用未激活用户要求装饰器
    decorators = [requires_unactivated]
    form = AccountActivationForm

    def __init__(self, account_activator_factory):
        """
        初始化方法

        :param account_activator_factory: 账户激活器工厂对象
        """
        self.account_activator_factory = account_activator_factory

    def get(self):
        """
        处理GET请求，显示账户激活页面

        :return: 渲染后的账户激活页面模板
        """
        return render_template("auth/account_activation.html", form=self.form())

    def post(self):
        """
        处理POST请求，验证账户激活表单并进行账户激活操作

        :return: 如果激活成功，显示账户已激活的提示并重新定向到论坛首页；如果验证失败或出现其他错误，重新渲染账户激活页面并显示相应错误信息
        """
        form = self.form()
        if form.validate_on_submit():
            token = form.token.data
            activator = self.account_activator_factory()
            try:
                activator.activate_account(token)
            except TokenError as e:
                form.populate_errors([("token", e.reason)])
            except ValidationError as e:
                flash(e.reason, "danger")
                return redirect(url_for("forum.index"))

            else:
                try:
                    db.session.commit()
                except Exception:
                    logger.exception("Database error while activating account")
                    db.session.rollback()
                    flash(
                        _(
                            "Could not activate account due to an unrecoverable error"
                        ),
                        "danger",
                    )

                    return redirect(url_for("auth.request_activation_token"))

                flash(
                    _("Your account has been activated and you can now login."),
                    "success",
                )
                return redirect(url_for("forum.index"))

        return render_template("auth/account_activation.html", form=form)


@impl(tryfirst=True)
def xfree_load_blueprints(app):
    """
    此函数用于在Flask应用中加载认证相关的蓝图。

    :param app: Flask应用对象
    """
    # 创建一个名为'auth'的蓝图
    auth = Blueprint("auth", __name__)

    def login_rate_limit():
        """
        动态地从数据库加载速率限制配置。

        返回值格式为：{请求次数}/{时间周期}分钟，例如"5/10minutes"表示10分钟内最多5次请求。
        """
        return "{count}/{timeout}minutes".format(
            count=xfree_config["AUTH_REQUESTS"],
            timeout=xfree_config["AUTH_TIMEOUT"],
        )

    def login_rate_limit_message():
        """
        显示用户再次访问请求资源所需的剩余时间。

        它获取当前的速率限制信息，计算出距离限制重置的剩余时间，并将其格式化后返回。
        """
        # 获取当前视图的速率限制（如果有）
        current_limit = getattr(g, "view_rate_limit", None)
        if current_limit is not None:
            # 获取窗口统计信息（开始时间和结束时间）
            window_stats = limiter.limiter.get_window_stats(*current_limit)
            # 根据开始时间计算重置时间
            reset_time = datetime.utcfromtimestamp(window_stats[0])
            # 计算距离重置时间的剩余时间
            timeout = reset_time - datetime.utcnow()
        return "{timeout}".format(timeout=format_timedelta(timeout))

    @auth.before_request
    def check_rate_limiting():
        """
        在每个对该蓝图的请求之前检查速率限制。

        如果在配置中禁用了认证速率限制（AUTH_RATELIMIT_ENABLED为False），则不进行检查直接返回None；
        否则，调用limiter.check()进行速率限制检查。
        """
        if not xfree_config["AUTH_RATELIMIT_ENABLED"]:
            return None
        return limiter.check()

    @auth.errorhandler(429)
    def login_rate_limit_error(error):
        """
        注册一个自定义的错误处理函数，用于处理'太多请求'（HTTP状态码429）错误。

        它渲染一个名为'errors/too_many_logins.html'的模板，并将剩余时间（error.description）传递给模板。
        """
        return render_template("errors/too_many_logins.html", timeout=error.description)

    # 对整个蓝图应用速率限制
    limiter.limit(login_rate_limit, error_message=login_rate_limit_message)(auth)

    # 注册视图函数到蓝图

    # 注册登出视图
    register_view(auth, routes=["/logout"], view_func=Logout.as_view("logout"))

    # 注册登录视图
    register_view(
        auth,
        routes=["/login"],
        view_func=Login.as_view(
            "login", authentication_manager_factory=authentication_manager_factory
        )
    )

    # 注册重新认证视图
    register_view(
        auth,
        routes=["/reauth"],
        view_func=Reauth.as_view(
            "reauth", reauthentication_factory=reauthentication_manager_factory
        )
    )

    # 注册注册视图
    register_view(
        auth,
        routes=["/register"],
        view_func=Register.as_view(
            "register", registration_service_factory=registration_service_factory
        )
    )

    # 注册忘记密码视图
    register_view(
        auth,
        routes=["/reset - password"],
        view_func=ForgotPassword.as_view(
            "forgot_password", password_reset_service_factory=reset_service_factory
        )
    )

    # 注册重置密码视图（带令牌参数）
    register_view(
        auth,
        routes=["/reset - password/<token>"],
        view_func=ResetPassword.as_view(
            "reset_password", password_reset_service_factory=reset_service_factory
        )
    )

    # 注册请求激活令牌视图
    register_view(
        auth,
        routes=["/activate"],
        view_func=RequestActivationToken.as_view(
            "request_activation_token",
            account_activator_factory=account_activator_factory
        )
    )

    # 注册激活账户视图（确认页面）
    register_view(
        auth,
        routes=["/activate/confirm"],
        view_func=ActivateAccount.as_view(
            "activate_account", account_activator_factory=account_activator_factory
        )
    )

    # 注册自动激活账户视图（带令牌参数）
    register_view(
        auth,
        routes=["/activate/confirm/<token>"],
        view_func=AutoActivateAccount.as_view(
            "autoactivate_account", account_activator_factory=account_activator_factory
        )
    )

    # 将认证蓝图注册到Flask应用，使用应用配置中的AUTH_URL_PREFIX作为前缀
    app.register_blueprint(auth, url_prefix=app.config["AUTH_URL_PREFIX"])